Apache HTTP ServerでALBのヘルスチェックのみBasic認証を回避するよう設定してみた
ALBからのヘルスチェック かつ ヘルスチェックパス宛の通信のみBasic認証を回避させたい
こんにちは、のんピ(@non____97)です。
皆さんはALBからのヘルスチェック かつ ヘルスチェックパス宛の通信のみBasic認証を回避させたいと思ったことはありますか? 私はあります。
「限られたメンバーにしか通信できないようにしたいが、送信元IPアドレスでは絞りにくい」といったときはBasic認証を使うことがあると思います。
Basic認証を回避できなければALBのヘルスチェックは401エラーとなってしまいます。
既に「ヘルスチェックパスの宛の通信は許可する」というアプローチでBasic認証を回避する記事は存在しています。
こちらの記事が投稿されてから10年以上経っており、Order/Allow/Denyディレクティブを使っていたりとしているので、Apache 2.4的な定義の仕方で実装してみます。
Apache 2.4系でOrder/Allow/Denyディレクティブを使うとAH01797: client denied by server configuration:
というメッセージがエラーログに出力され、目障りなのでなんとかしたいところです。
なお、「大量の送信元IPアドレス、ユーザーエージェント、リクエストパスの組み合わせでBasic認証をしたい」という訳でなければ、Lambda関数でBasic認証をかける方が楽です。
いきなりまとめ
- Requireディレクティブを使おう
- 「全ての条件を満たす」を定義したい場合はRequireAnyディレクティブを使おう
検証環境
検証環境は以下のとおりです。
WebサーバーとしてApache HTTP Serverをインストールしました。また、なんとなくApache Tomcatもセットアップしました。
環境はAWS CDKでデプロイしました。使用したコードは以下リポジトリに保存しています。
UserDataとAssetを組み合わせると、簡単に大量の設定ファイルをEC2インスタンスにアップロードすることが出来て非常に便利です。
// Assets const httpdAssets = new cdk.aws_s3_assets.Asset(this, "HttpdAssets", { path: path.join(__dirname, "../ec2/httpd"), }); const tomcatAssets = new cdk.aws_s3_assets.Asset(this, "TomcatAssets", { path: path.join(__dirname, "../ec2/tomcat"), }); // IAM Role const role = new cdk.aws_iam.Role(this, "Role", { assumedBy: new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com"), managedPolicies: [ new cdk.aws_iam.ManagedPolicy(this, "Policy", { statements: [ new cdk.aws_iam.PolicyStatement({ effect: cdk.aws_iam.Effect.ALLOW, resources: [ `arn:aws:s3:::${httpdAssets.s3BucketName}/*`, `arn:aws:s3:::${tomcatAssets.s3BucketName}/*`, ], actions: ["s3:GetObject"], }), ], }), ], }); // User data const userData = cdk.aws_ec2.UserData.forLinux(); userData.addCommands( fs.readFileSync( path.join(__dirname, "../ec2/user-data/default.sh"), "utf8" ) ); userData.addS3DownloadCommand({ bucket: httpdAssets.bucket, bucketKey: httpdAssets.s3ObjectKey, localFile: "/tmp/assets/httpd.zip", }); userData.addS3DownloadCommand({ bucket: tomcatAssets.bucket, bucketKey: tomcatAssets.s3ObjectKey, localFile: "/tmp/assets/tomcat.zip", }); userData.addCommands( fs.readFileSync( path.join(__dirname, "../ec2/user-data/setting-packages.sh"), "utf8" ) );
肝心のApache HTTP Serverの設定は以下のとおりです。
<VirtualHost *:80> ServerName sample1.web.non-97.net DocumentRoot /var/www/html/sample1 ProxyPass /tomcat ajp://localhost:8009/ secret=AjpSecret5 ProxyPassReverse /tomcat ajp://localhost:8009/ secret=AjpSecret5 ErrorLog /var/log/httpd/sample1_error_log CustomLog /var/log/httpd/sample1_access_log combined Timeout 120 KeepAlive On KeepAliveTimeout 120 <Directory /var/www/html/sample1> Options FollowSymLinks AllowOverride All Require all denied </Directory> <Location / > SetEnvIf User-Agent "^ELB-HealthChecker.*$" elbHealthCheckUserAgent SetEnvIf Request_URI "^/tomcat$" elbHealthCheckUri <RequireAll> Require env elbHealthCheckUserAgent Require env elbHealthCheckUri </RequireAll> <RequireAny> AuthType Basic AuthName "Input your ID and Password." AuthGroupFile /dev/null AuthUserFile "/etc/httpd/conf.d/.htpasswd" Require valid-user </RequireAny> </Location> </VirtualHost>
ドキュメントルート/var/www/html/sample1
のDirectoryディレクティブで、Require all denied
で拒否するようにしています。
Locationディレクティブの/
にて以下のand条件に当てはまらない場合はBasic認証をするように定義をしています。
- リクエストパスがALBのヘルスチェックパスである
/tomcat
- ユーザーエージェントがALBのヘルスチェックのものである
ELB-HealthChecker
SetEnvIf Request_URI /tomcat elbHealthCheckUri
だと、/tomcats
や/tomcat/path
など前方一致する場合もマッチしてしまうので、完全一致するように明示します。
SetEnvIf
とRequire env
を組み合わせれば大概のものは定義できそうな気がします。Requireディレクティブはmod_authz_core
モジュールをインポートすると使えます。
動作確認
AWS CDKでリソースをデプロイした後、動作確認をします。
アクセスログを確認すると、以下のようにALBのヘルスチェックの通信が記録されていました。
$ less /var/log/httpd/sample1_access_log 10.10.10.14 - - - [28/Oct/2023:07:56:19 +0000] "GET /tomcat HTTP/1.1" 200 65 2630 "-" "ELB-HealthChecker/2.0" 10.10.10.57 - - - [28/Oct/2023:07:56:19 +0000] "GET /tomcat HTTP/1.1" 200 65 1974 "-" "ELB-HealthChecker/2.0" 10.10.10.14 - - - [28/Oct/2023:07:56:29 +0000] "GET /tomcat HTTP/1.1" 200 65 1633 "-" "ELB-HealthChecker/2.0" 10.10.10.57 - - - [28/Oct/2023:07:56:29 +0000] "GET /tomcat HTTP/1.1" 200 65 2283 "-" "ELB-HealthChecker/2.0"
実際にアクセスして、Basic認証を回避できるパターンを確認しましょう。
# ユーザーエージェントを指定しない かつ ALBのヘルスチェックパス以外 $ curl -I https://sample1.web.non-97.net HTTP/2 401 date: Sat, 28 Oct 2023 11:56:18 GMT content-type: text/html; charset=iso-8859-1 server: Apache www-authenticate: Basic realm="Input your ID and Password." # ユーザーエージェントを指定しない かつ ALBのヘルスチェックパス $ curl -I https://sample1.web.non-97.net/tomcat HTTP/2 401 date: Sat, 28 Oct 2023 11:56:24 GMT content-type: text/html; charset=iso-8859-1 server: Apache www-authenticate: Basic realm="Input your ID and Password." # ユーザーエージェントを指定 かつ ALBのヘルスチェックパス以外 $ curl -I https://sample1.web.non-97.net/ -A "ELB-HealthChecker/2.0" HTTP/2 401 date: Sat, 28 Oct 2023 11:56:32 GMT content-type: text/html; charset=iso-8859-1 server: Apache www-authenticate: Basic realm="Input your ID and Password." # ユーザーエージェントを指定 かつ ALBのヘルスチェックパス $ curl -I https://sample1.web.non-97.net/tomcat -A "ELB-HealthChecker/2.0" HTTP/2 200 date: Sat, 28 Oct 2023 11:56:38 GMT content-type: text/html;charset=UTF-8 content-length: 65 server: Apache set-cookie: JSESSIONID=0053D0857967DD0A5E9F719206A02733; Path=/; HttpOnly x-frame-options: SAMEORIGIN x-content-type-options: nosniff x-xss-protection: 1; mode=block # ユーザーエージェントを指定 かつ ALBのヘルスチェックパスの前方一致 $ curl -I https://sample1.web.non-97.net/tomcats -A "ELB-HealthChecker/2.0" HTTP/2 401 date: Sat, 28 Oct 2023 11:57:03 GMT content-type: text/html; charset=iso-8859-1 server: Apache www-authenticate: Basic realm="Input your ID and Password."
ALBからのヘルスチェック かつ ヘルスチェックパス宛の通信のみBasic認証を回避できていますね。
もちろん、条件に当てはまらない場合でもBasic認証の認証情報を渡してあげれば、ステータスコード200が返ってきます。
$ curl -I https://sample1.web.non-97.net/ -u test-user:test-p@ssw0rd HTTP/2 200 date: Sat, 28 Oct 2023 11:56:50 GMT content-type: text/html; charset=UTF-8 content-length: 36 server: Apache last-modified: Sat, 28 Oct 2023 11:23:57 GMT etag: "24-608c50a7308a1" accept-ranges: bytes x-frame-options: SAMEORIGIN x-content-type-options: nosniff x-xss-protection: 1; mode=block
複雑な条件でリスナールール数が非常に多くなるようであればバックエンド側でBasic認証の設定をするのが良さそう
Apache HTTP ServerでALBのヘルスチェックのみBasic認証を回避するよう設定してみました。
Basic認証を回避させる条件がシンプルであれば、ALBのリスナールールとLambda関数を組みわせるパターンが良いでしょう。
ただし、条件が複雑でルール数が非常に多くなるようであればバックエンド側でBasic認証の設定をするのが良さそうです。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!